#include "OpenGLShader.h"
#include "hzpch.h"

#include <fstream>

#include "glad/glad.h"

#include <glm/gtc/type_ptr.hpp>

namespace Hazel
{

static GLenum ShaderTypeFromString(const std::string &type)
{
    if (type == "vertex")
        return GL_VERTEX_SHADER;
    if (type == "fragment" || type == "pixel")
        return GL_FRAGMENT_SHADER;

    HZ_CORE_ASSERT(false, "Unknown shader type!");
    return 0;
}

OpenGLShader::OpenGLShader(const std::string &name, const std::string &vertexSrc, const std::string &fragmentSrc)
    : m_name(name)
{
    HZ_PROFILE_FUNC();

    std::unordered_map<GLenum, std::string> sources;
    sources[GL_VERTEX_SHADER] = vertexSrc;
    sources[GL_FRAGMENT_SHADER] = fragmentSrc;

    Compile(sources);
}
OpenGLShader::OpenGLShader(const std::string &filepath)
{
    HZ_PROFILE_FUNC();

    std::string source = ReadFile(filepath);
    auto shaderSources = PreProcess(source);

    Compile(shaderSources);

    // assert/shader/texture.shader.glsl
    auto lastSlash = filepath.find_last_of("/\\");
    lastSlash = lastSlash == std::string::npos ? 0 : lastSlash + 1;
    auto lastDot = filepath.rfind('.');
    auto count = lastDot == std::string::npos ? filepath.size() - lastSlash : lastDot - lastSlash;
    m_name = filepath.substr(lastSlash, count);
}
OpenGLShader::~OpenGLShader()
{
    HZ_PROFILE_FUNC();

    glDeleteProgram(m_RendererID);
}
void OpenGLShader::Bind() const
{
    HZ_PROFILE_FUNC();

    glUseProgram(m_RendererID);
}
void OpenGLShader::Unbind() const
{
    HZ_PROFILE_FUNC();

    glUseProgram(0);
}
void OpenGLShader::SetInt(const std::string name, int value)
{
    HZ_PROFILE_FUNC();

    UploadUniformInt(name, value);
}
void OpenGLShader::SetIntArray(const std::string name, int *values, uint32_t count)
{
    HZ_PROFILE_FUNC();

    UploadUniformIntArray(name, values, count);
}
void OpenGLShader::SetFloat(const std::string name, float value)
{
    HZ_PROFILE_FUNC();

    UploadUniformFloat(name, value);
}
void OpenGLShader::SetFloat3(const std::string name, const glm::vec3 &value)
{
    HZ_PROFILE_FUNC();

    UploadUniformFloat3(name, value);
}
void OpenGLShader::SetFloat4(const std::string name, const glm::vec4 &value)
{
    HZ_PROFILE_FUNC();

    UploadUniformFloat4(name, value);
}
void OpenGLShader::SetMat4(const std::string name, const glm::mat4 &value)
{
    HZ_PROFILE_FUNC();

    UploadUniformMat4(name, value);
}
void OpenGLShader::UploadUniformMat3(const std::string &name, const glm::mat3 &matrix)
{
    GLint location = glGetUniformLocation(m_RendererID, name.c_str());
    glUniformMatrix3fv(location, 1, GL_FALSE, glm::value_ptr(matrix));
}
void OpenGLShader::UploadUniformMat4(const std::string &name, const glm::mat4 &matrix)
{

    GLint location = glGetUniformLocation(m_RendererID, name.c_str());
    glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(matrix));
}

void OpenGLShader::UploadUniformInt(const std::string &name, int value)
{

    GLint location = glGetUniformLocation(m_RendererID, name.c_str());
    glUniform1i(location, value);
}

void OpenGLShader::UploadUniformIntArray(const std::string &name, int *values, uint32_t count)
{
    GLint location = glGetUniformLocation(m_RendererID, name.c_str());
    glUniform1iv(location, count, values);
}

void OpenGLShader::UploadUniformFloat(const std::string &name, float value)
{
    GLint location = glGetUniformLocation(m_RendererID, name.c_str());
    glUniform1f(location, value);
}

void OpenGLShader::UploadUniformFloat2(const std::string &name, const glm::vec2 &values)
{

    GLint location = glGetUniformLocation(m_RendererID, name.c_str());
    glUniform2f(location, values.x, values.y);
}

void OpenGLShader::UploadUniformFloat3(const std::string &name, const glm::vec3 &values)
{
    GLint location = glGetUniformLocation(m_RendererID, name.c_str());
    glUniform3f(location, values.x, values.y, values.z);
}

void OpenGLShader::UploadUniformFloat4(const std::string &name, const glm::vec4 &values)
{
    GLint location = glGetUniformLocation(m_RendererID, name.c_str());
    glUniform4f(location, values.x, values.y, values.z, values.w);
}
std::string OpenGLShader::ReadFile(const std::string &filepath)
{
    HZ_PROFILE_FUNC();

    std::string result;
    std::ifstream in(filepath, std::ios::in | std::ios::binary);
    if (in)
    {
        in.seekg(0, std::ios::end);
        size_t size = in.tellg();
        if (size != -1)
        {
            result.resize(size);
            in.seekg(0, std::ios::beg);
            in.read(&result[0], size);
            in.close();
        }
        else
        {
            HZ_CORE_ERROR("Could not read from file '{0}'", filepath);
        }
    }
    else
    {
        HZ_CORE_ERROR("Could not open file '{0}'", filepath);
    }
    return result;
}
std::unordered_map<GLenum, std::string> OpenGLShader::PreProcess(const std::string &source)
{
    HZ_PROFILE_FUNC();

    std::unordered_map<GLenum, std::string> shaderSources;

    const char *typeToken = "#type";
    size_t typeTokenLength = strlen(typeToken);
    size_t pos = source.find(typeToken, 0);

    while (pos != std::string::npos)
    {
        size_t eol = source.find_first_of("\r\n", pos);
        HZ_CORE_ASSERT(eol != std::string::npos, "Syntax Error");
        size_t begin = pos + typeTokenLength + 1;
        std::string type = source.substr(begin, eol - begin);
        HZ_CORE_ASSERT(ShaderTypeFromString(type), "Invalid shader type specified!");

        size_t nextLinePos = source.find_first_not_of("\r\n", eol);
        pos = source.find(typeToken, nextLinePos);
        shaderSources[ShaderTypeFromString(type)] =
            source.substr(nextLinePos, pos - (nextLinePos == std::string::npos ? source.size() - 1 : nextLinePos));
        std::string str =
            source.substr(nextLinePos, pos - (nextLinePos == std::string::npos ? source.size() - 1 : nextLinePos));
        // std::cout<<source.substr(nextLinePos,
        //     pos-(nextLinePos==std::string::npos?source.size()-1:nextLinePos))<<'\n';
    }
    return shaderSources;
}

void OpenGLShader::Compile(const std::unordered_map<GLenum, std::string> &shaderSources)
{
    HZ_PROFILE_FUNC();

    // Vertex and fragment shaders are successfully compiled.
    // Now time to link them together into a program.
    // Get a program object.
    GLuint program = glCreateProgram();
    HZ_CORE_ASSERT(shaderSources.size() <= 2, "we only support only 2 shaders for now!");
    std::array<GLenum, 2> glShaderIDs;
    int glShaderIDIndex = 0;
    for (auto &kv : shaderSources)
    {
        GLenum ShaderType = kv.first;
        const std::string source = kv.second;

        GLuint shader = glCreateShader(ShaderType);

        // Send the vertex shader source code to GL
        // Note that std::string's .c_str is NULL character terminated.
        const GLchar *sourceCStr = source.c_str();
        glShaderSource(shader, 1, &sourceCStr, 0);

        // Compile the vertex shader
        glCompileShader(shader);

        GLint isCompiled = 0;
        glGetShaderiv(shader, GL_COMPILE_STATUS, &isCompiled);
        if (isCompiled == GL_FALSE)
        {
            GLint maxLength = 0;
            glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength);

            // The maxLength includes the NULL character
            std::vector<GLchar> infoLog(maxLength);
            glGetShaderInfoLog(shader, maxLength, &maxLength, &infoLog[0]);

            // We don't need the shader anymore.
            glDeleteShader(shader);

            HZ_CORE_ERROR("Shader compilation failure!");
            HZ_CORE_ERROR("{0}", infoLog.data());
            break;
        }

        // Attach our shaders to our program
        glAttachShader(program, shader);
        glShaderIDs[glShaderIDIndex++] = shader;
    }
    // Create an empty vertex shader handle

    // Link our program
    glLinkProgram(program);

    // Note the different functions here: glGetProgram* instead of glGetShader*.
    GLint isLinked = 0;
    glGetProgramiv(program, GL_LINK_STATUS, (int *)&isLinked);
    if (isLinked == GL_FALSE)
    {
        GLint maxLength = 0;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &maxLength);

        // The maxLength includes the NULL character
        std::vector<GLchar> infoLog(maxLength);
        glGetProgramInfoLog(program, maxLength, &maxLength, &infoLog[0]);

        // We don't need the program anymore.
        glDeleteProgram(program);
        // Don't leak shaders either.
        for (auto id : glShaderIDs)
            glDeleteShader(id);

        HZ_CORE_ERROR("Shader link failure!");
        HZ_CORE_ERROR("{0}", infoLog.data());
        return;
    }

    // Always detach shaders after a successful link.
    for (auto id : glShaderIDs)
        glDetachShader(program, id);

    m_RendererID = program;
}
} // namespace Hazel